Sieci komputerowe — ćwiczenia 3
Temat zajęć: Programowanie gniazd BSD c.d.
Literatura:
- R. Stevens, "Programowanie zastosowań sieciowych w systemie Unix"
- A. Jones, J. Ohlund, "Programowanie sieciowe Microsoft Windows"
- C. Petzold, "Programowanie Windows"
- M. Gabassi, B. Dupouy, "Przetwarzanie rozproszone w systemie Unix"
- E. Harold, "Java: programowanie sieciowe"
- R. Stevens, "Biblia TCP/IP" (tom 1 - Protokoły i tom 2 - Implementacje)
- C. Hunt, "TCP/IP - administracja sieci"
- V. Toth, "Programowanie Windows 98/NT - księga eksperta"
- Dokumenty RFC wersja on-line
Wykorzystanie funkcji select
Funkcja select umożliwia nadzorowanie zbioru deskryptorów pod względem możliwości odczytu, zapisu bądź wystąpienia sytuacji wyjątkowych. Formalnie prototyp funkcji wygląda następująco (definicja w sys/select.h):int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)Funkcja przyjmuje wspomniane trzy zbiory deskryptorów, jednak nie ma obowiązku określania ich wszystkich (można w miejsce odp. zbioru deskryptorów podać NULL - wówczas dany zbiór nie będzie nadzorowany przez select).
W celu umożliwienia nasłuchu na dwóch (lub więcej) gniazdach jednocześnie, należy postępować wg następującego schematu:W przypadku gniazd TCP select zwróci gotowość deskryptora jeśli możliwe jest wywołanie na gnieździe funkcji accept (gniazdo nasłuchujące) lub recv (gniazdo komunikacyjne) bez blokowania aplikacji (czyli w momencie, w którym istnieje oczekujące połączenie na gnieździe nasłuchującym lub gdy czekają dane w buforze na gnieździe komunikacyjnym).
- wstaw deskryptory gniazd (g1 i g2) do zbioru readfds
- ustaw timeout
- wywołaj select na zbiorze readfds
- jeśli select zwrócił wartość dodatnią, to
- sprawdź czy g1 jest ustawiony w readfds, jeśli tak, to obsłuż odczyt na gnieździe g1
- sprawdź czy g2 jest ustawiony w readfds, jeśli tak, to obsłuż odczyt na gnieździe g2
- ew. powrót do 1
W przypadku gniazd UDP select zwróci gotowość deskryptora jeśli możliwe jest nieblokujące wywołanie recvfrom (w buforze odczytu oczekuje datagram).
Istotne informacje na temat select (dostępne w man pages):
- do czyszczenia zbioru deskryptorów służy FD_ZERO(fd_set *fds)
- do dodawania deskryptora do zbioru służy FD_SET(int fd, fd_set *fds)
- do usuwania deskryptora ze zbioru służy FD_CLR(int fd, fd_set *fds)
- do sprawdzania przynależności deskryptora do zbioru służy FD_ISSET(int fd, fd_set *fds)
- funkcja select jako swój wynik zwraca liczbę "gotowych" deskryptorów
- pierwszym parametrem select musi być największa wartość deskryptora ze zbiorów powiększona o 1, a NIE liczba deskryptorów (częsty błąd!)
- jeśli jako timeout podamy NULL, select wraca natychmiast, informując jaki jest bieżący stan deskryptorów
- jeśli jako timeout podamy niezerowy czas, select wraca po upływie tego czasu lub po wystąpieniu zdarzenia na deskryptorze (zależy co nastąpi wcześniej)
- select może (zależnie od implementacji) zmienić wartość timeout, zatem należy zawsze ustawiać czas oczekiwania na nowo przed wywołaniem funkcji
- jeśli jako timeout podamy zerowy czas (ale nie NULL), select wróci natychmiast (rodzaj pollingu), jeżeli natomiast wpiszemy w miejsce timeval NULL, to select wróci dopiero po wystąpieniu zdarzenia (innymi słowy zerowy czas oczekiwania oznacza czekanie w nieskończoność na wystąpienie zdarzenia)
- struktura timeval posiada pola tv_sec (sekundy) i tv_usec (mikrosekundy)
Przykład 1
Przykład zawiera implementację serwera, który nasłuchuje zarówno na TCP, jak i UDP. Program ten może służyć jako serwer zarówno dla klienta z Przykładu 5 z Ćwiczeń 1, jak i dla klienta z Przykładu 1 z Ćwiczeń 2 (proszę sprawdzić oba, uruchamiając je jednocześnie dla jednej instancji poniższego serwera).
Plik c3p1.c pobierz#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
void DrukujNadawce(struct sockaddr_in *adres)
{
printf("Wiadomosc od %s:%d",
inet_ntoa(adres->sin_addr),
ntohs(adres->sin_port)
);
}
void ObsluzTCP(int gniazdo, struct sockaddr_in *adres)
{
int nowe_gniazdo;
char bufor[1024];
socklen_t dladr = sizeof(struct sockaddr_in);
nowe_gniazdo =
accept(gniazdo, (struct sockaddr*) adres,
&dladr);
if (nowe_gniazdo < 0)
{
printf("Bledne polaczenie (accept < 0)\n");
return;
}
memset(bufor, 0, 1024);
while (recv(nowe_gniazdo, bufor, 1024, 0) <= 0);
DrukujNadawce(adres);
printf("[TCP]: %s\n", bufor);
close(nowe_gniazdo);
}
void ObsluzUDP(int gniazdo, struct sockaddr_in *adres)
{
char bufor[1024];
socklen_t dladr = sizeof(struct sockaddr_in);
memset(bufor, 0, 1024);
recvfrom(gniazdo, bufor, 1024, 0, (struct sockaddr*) adres,
&dladr);
DrukujNadawce(adres);
printf("[UDP]: %s\n", bufor);
}
void ObsluzObaProtokoly(int gniazdoTCP, int gniazdoUDP,
struct sockaddr_in *adres)
{
fd_set readfds;
struct timeval timeout;
unsigned long proba;
int maxgniazdo;
maxgniazdo = (gniazdoTCP > gniazdoUDP ?
gniazdoTCP+1 : gniazdoUDP+1);
proba = 0;
while(1)
{
FD_ZERO(&readfds);
FD_SET(gniazdoTCP, &readfds);
FD_SET(gniazdoUDP, &readfds);
timeout.tv_sec = 1;
timeout.tv_usec = 0;
if (select(maxgniazdo, &readfds, NULL, NULL, &timeout) > 0)
{
proba = 0;
if (FD_ISSET(gniazdoTCP, &readfds))
ObsluzTCP(gniazdoTCP, adres);
if (FD_ISSET(gniazdoUDP, &readfds))
ObsluzUDP(gniazdoUDP, adres);
}
else
{
proba++;
printf("Czekam %lu sekund i nic ...\n", proba);
}
}
}
int main(void)
{
struct sockaddr_in bind_me_here;
int gt, gu, port;
printf("Numer portu: ");
scanf("%d", &port);
gt = socket(PF_INET, SOCK_STREAM, 0);
gu = socket(PF_INET, SOCK_DGRAM, 0);
bind_me_here.sin_family = AF_INET;
bind_me_here.sin_port = htons(port);
bind_me_here.sin_addr.s_addr = INADDR_ANY;
if (bind(gt,(struct sockaddr*) &bind_me_here,
sizeof(struct sockaddr_in)) < 0)
{
printf("Bind na TCP nie powiodl sie.\n");
return 1;
}
if (bind(gu,(struct sockaddr*) &bind_me_here,
sizeof(struct sockaddr_in)) < 0)
{
printf("Bind na UDP nie powiodl sie.\n");
return 1;
}
listen(gt, 10);
ObsluzObaProtokoly(gt, gu, &bind_me_here);
return 0;
}Serwery równoległe
Już na pierwszych zajęciach omawialiśmy problem obsługi wielu łączących się klientów w tym samym czasie. Wiemy, że dla protokołu TCP funkcja listen umożliwia ustalenie wielkości kolejki oczekujących połączeń. Zatem w czasie, gdy usługa (serwer) komunikuje się z jednym klientem, inni klienci próbujący połączyć się z gniazdem usługi zostają umieszczeni w kolejce. Jeśli jednak natura usługi wymaga długotrwałej komunikacji z jednym klientem, może to doprowadzić do niedostępności usługi dla innych klientów przez dłuższy czas. Istnieje proste rozwiązanie tego problemu, które opiera się na właściwości gniazd TCP. Otóż w momencie, gdy funkcja accept zwróci deskryptor nowego gniazda (jakiś klient wykonał connect), można zrównoleglić program usługi (np. wykonując fork) i w procesie potomnym obsłużyć właśnie połączonego klienta, zaś w procesie macierzystym powrócić do oczekiwania na kolejne połączenia (czyli ponownie wykonać accept). Scenariusz taki możliwy jest tylko w przypadku protokołu TCP (nie UDP), ponieważ protokół ten zapewnia wzajemnie jednoznaczne skojarzenie gniazd dla pary connect, accept. Innymi słowy, system po stronie usługi jest w stanie rozróżnić, czy przychodzący segment TCP dotyczy już toczącej się "rozmowy", czy jest to początek nowego połączenia (nowy connect od innego klienta).Przykład 2
Załóżmy, że implementujemy usługę (i jej klientów), której zadaniem jest przesyłanie na żądanie zawartości pliku o podanej przez klienta ścieżce. Klient łączy się na ustalony port usługi (np. 21212) i przesyła ścieżkę do pliku (w systemie plików maszyny, na której działa serwer usługi), którego zawartość chce pobrać. Usługa odsyła najpierw wielkość pliku (long w formacie sieci), a następnie samą zawartość pliku. Zauważmy, że przesłanie pliku może zająć dłuższy czas (w zależności od rozmiaru pliku i dostępnej przepustowości łącza), a dodatkowe opóźnienia może wprowadzać sama konstrukcja kodu klienta. Jeśli bowiem po połączeniu program kliencki będzie czekał, aż użytkownik poda ścieżkę do pliku, to może on blokować usługę przez nieokreślony z góry czas. Zatem aby umożliwić innym klientom korzystanie z usługi, po nawiązaniu połączenia przez klienta serwer utworzy swój proces potomny, który zajmie się "rozdawaniem" plików konkretnemu klientowi, podczas gdy proces macierzysty powróci do accept i będzie czekał na kolejnych klientów.
Plik c3p2a.c pobierz#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#define PORT htons(21212)
void ObsluzPolaczenie(int gn)
{
char sciezka[512];
long dl_pliku, wyslano, wyslano_razem, przeczytano;
struct stat fileinfo;
FILE* plik;
unsigned char bufor[1024];
memset(sciezka, 0, 512);
if (recv(gn, sciezka, 512, 0) <= 0)
{
printf("Potomny: blad przy odczycie sciezki\n");
return;
}
printf("Potomny: klient chce plik %s\n", sciezka);
if (stat(sciezka, &fileinfo) < 0)
{
printf("Potomny: nie moge pobrac informacji o pliku\n");
return;
}
if (fileinfo.st_size == 0)
{
printf("Potomny: rozmiar pliku 0\n");
return;
}
printf("Potomny: dlugosc pliku: %d\n", fileinfo.st_size);
dl_pliku = htonl((long) fileinfo.st_size);
if (send(gn, &dl_pliku, sizeof(long), 0) != sizeof(long))
{
printf("Potomny: blad przy wysylaniu wielkosci pliku\n");
return;
}
dl_pliku = fileinfo.st_size;
wyslano_razem = 0;
plik = fopen(sciezka, "rb");
if (plik == NULL)
{
printf("Potomny: blad przy otwarciu pliku\n");
return;
}
while (wyslano_razem < dl_pliku)
{
przeczytano = fread(bufor, 1, 1024, plik);
wyslano = send(gn, bufor, przeczytano, 0);
if (przeczytano != wyslano)
break;
wyslano_razem += wyslano;
printf("Potomny: wyslano %d bajtow\n", wyslano_razem);
}
if (wyslano_razem == dl_pliku)
printf("Potomny: plik wyslany poprawnie\n");
else
printf("Potomny: blad przy wysylaniu pliku\n");
fclose(plik);
return;
}
int main(void)
{
int gn_nasluch, gn_klienta;
struct sockaddr_in adr;
socklen_t dladr = sizeof(struct sockaddr_in);
gn_nasluch = socket(PF_INET, SOCK_STREAM, 0);
adr.sin_family = AF_INET;
adr.sin_port = PORT;
adr.sin_addr.s_addr = INADDR_ANY;
memset(adr.sin_zero, 0, sizeof(adr.sin_zero));
if (bind(gn_nasluch, (struct sockaddr*) &adr, dladr) < 0)
{
printf("Glowny: bind nie powiodl sie\n");
return 1;
}
listen(gn_nasluch, 10);
while(1)
{
dladr = sizeof(struct sockaddr_in);
gn_klienta = accept(gn_nasluch, (struct sockaddr*) &adr, &dladr);
if (gn_klienta < 0)
{
printf("Glowny: accept zwrocil blad\n");
continue;
}
printf("Glowny: polaczenie od %s:%u\n",
inet_ntoa(adr.sin_addr),
ntohs(adr.sin_port)
);
printf("Glowny: tworze proces potomny\n");
if (fork() == 0)
{
/* proces potomny */
printf("Potomny: zaczynam obsluge\n");
ObsluzPolaczenie(gn_klienta);
printf("Potomny: zamykam gniazdo\n");
close(gn_klienta);
printf("Potomny: koncze proces\n");
exit(0);
}
else
{
/* proces macierzysty */
printf("Glowny: wracam do nasluchu\n");
continue;
}
}
return 0;
}
Plik c3p2b.c pobierz#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netdb.h>
#define IP(H) *((unsigned long*) (H)->h_addr_list[0])
int main(void)
{
int gn;
struct sockaddr_in adr;
int port;
struct hostent *h;
char nazwa[512];
char bufor[1025];
char sciezka[512];
long dl_pliku, odebrano, odebrano_razem;
printf("Nazwa hosta / adres IP: ");
scanf("%s", nazwa);
h = gethostbyname(nazwa);
if (h == NULL)
{
printf("Nieznany host\n");
return 1;
}
printf("Numer portu: ");
scanf("%d", &port);
adr.sin_family = AF_INET;
adr.sin_port = htons(port);
adr.sin_addr.s_addr = IP(h);
printf("Lacze sie z %s:%d\n",
inet_ntoa(adr.sin_addr),
port);
gn = socket(PF_INET, SOCK_STREAM, 0);
if (connect(gn, (struct sockaddr*) &adr, sizeof(adr))<0)
{
printf("Nawiazanie polaczenia nie powiodlo sie\n");
close(gn);
return 1;
}
printf("Polaczenie nawiazane\n");
printf("Podaj sciezke do pliku: \n");
memset(sciezka, 0, 512);
scanf("%s",sciezka);
printf("Wysylam sciezke\n");
if (send(gn, sciezka, strlen(sciezka), 0) != strlen(sciezka))
{
printf("Blad przy wysylaniu sciezki\n");
close(gn);
return 1;
}
printf("Sciezka wyslana. Odczytuje dlugosc pliku.\n");
if (recv(gn, &dl_pliku, sizeof(long), 0) != sizeof(long))
{
printf("Blad przy odbieraniu dlugosci\n");
printf("Moze plik nie istnieje?\n");
close(gn);
return 1;
}
dl_pliku = ntohl(dl_pliku);
printf("Plik ma dlugosc %d\n", dl_pliku);
printf("----- ZAWARTOSC PLIKU -----\n");
odebrano_razem = 0;
while (odebrano_razem < dl_pliku)
{
memset(bufor, 0, 1025);
odebrano = recv(gn, bufor, 1024, 0);
if (odebrano < 0)
break;
odebrano_razem += odebrano;
fputs(bufor, stdout);
}
close(gn);
if (odebrano_razem != dl_pliku)
printf("*** BLAD W ODBIORZE PLIKU ***\n");
else
printf("*** PLIK ODEBRANY POPRAWNIE ***\n");
return 0;
}Zadanie 1
Możliwość zrównoleglania obsługi wielu klientów jest niewątpliwą zaletą w kontekście programowania gniazd. Jednak tworzenie wielu procesów potomnych bez żadnej kontroli ich liczby jest niedopuszczalne z punktu widzenia bezpieczeństwa systemu. Dlatego w większości przypadków usług sieciowych określa się maksymalną liczbę klientów, jacy mogą być obsługiwani przez usługę w tym samym czasie (innymi słowy maksymalną liczbę procesów potomnych usługi). Proszę zmodyfikować kod z przykładu 2 tak, aby usługa obsługiwała nie więcej niż 10 klientów jednocześnie.
Podpowiedź: należy wprowadzić licznik procesów potomnych i wykorzystać funkcję systemową wait (proszę sprawdzić man 2 wait).Zadanie (domowe) — powtórka BSD
W czasie następnych zajęć (ćwiczenia 4) za tydzień, na hoście o adresie 150.254.77.129 na porcie podanym na tablicy (w przykładach 4444), nasłuchiwać będzie pewien program — serwer. Należy zaimplementować program kliencki, który zrealizuje następujący protokół:
Proszę zaimplementować do niego program kliencki. Program powinien zakodować numer indeksu autora jako long w formacie sieci i wysłać pod wskazany adres i numer portu. Następnie powinien odebrać pewną liczbę (również jako long w formacie sieci), zdekodować ją do formatu hosta, dodać do niej 1, ponownie zakodować do formatu sieci i odesłać na: 150.254.77.129:port. Wszystko musi odbywać się w jednej sesji (za pomocą jednego połączonego gniazda). Numery indeksów wszystkich studentów, których programy poprawnie zrealizują ten protokół, zostają zapisane w zbiorze, który zweryfikuję na końcu zajęć. W celu wyjaśnienia ew. wątpliwości, oraz aby umożliwić testowanie swoich programów klienckich we własnym zakresie, poniżej podaję kod nasłuchującego programu.
Plik c1za.c pobierz#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
struct sockaddr_in endpoint;
FILE *flog;
char myhostname[1024];
int main(int argc, char **argv) {
long lnIn1, lnIn2, lhIn1, lhIn2, lhOut, lnOut;
int sdServerSocket, sdConnection, retval;
socklen_t sin_size;
struct sockaddr_in incoming;
struct hostent *heLocalHost;
char sign;
sin_size = sizeof(struct sockaddr_in);
sdServerSocket = socket(PF_INET, SOCK_STREAM, 0);
gethostname(myhostname, 1023);
heLocalHost = gethostbyname(myhostname);
endpoint.sin_family = AF_INET;
endpoint.sin_port = htons(14444);
endpoint.sin_addr = *(struct in_addr*)
heLocalHost->h_addr;
memset(&(endpoint.sin_zero),0,8);
printf("slucham na %s:%d\n",
inet_ntoa(endpoint.sin_addr),
ntohs(endpoint.sin_port));
retval = bind(sdServerSocket,
(struct sockaddr*) &endpoint,
sizeof(struct sockaddr));
if (retval < 0) {
printf("bind nie powiodl sie\n");
return 1;
}
listen(sdServerSocket, 10);
sin_size = sizeof(struct sockaddr_in);
while ((sdConnection =
accept(sdServerSocket,
(struct sockaddr*) &incoming,
&sin_size))
> 0) {
printf("Polaczenie z %s:%d\n",
inet_ntoa(incoming.sin_addr),
ntohs(incoming.sin_port));
if (recv(sdConnection, &lnIn1, sizeof(long),0)
!= sizeof(long)) {
printf("pierwszy recv nie powiodl sie\n");
close(sdConnection);
continue;
}
lhIn1 = ntohl(lnIn1);
lhOut = random();
lnOut = htonl(lhOut);
if (send(sdConnection, &lnOut, sizeof(long), 0)
!= sizeof(long)) {
printf("send nie powiodl sie\n");
close(sdConnection);
continue;
}
if (recv(sdConnection, &lnIn2, sizeof(long), 0)
!= sizeof(long)) {
printf("drugi recv nie powiodl sie\n");
close(sdConnection);
continue;
}
lhIn2 = ntohl(lnIn2);
flog = fopen("zad.txt","a");
if (lhIn2 == lhOut + 1) sign = '+';
else sign = '-';
fprintf(flog,"%c %ld from %s:%d : %ld, %ld\n",
sign,
lhIn1,
inet_ntoa(incoming.sin_addr),
ntohs(incoming.sin_port),
lhOut,
lhIn2);
close(sdConnection);
fflush(flog);
fclose(flog);
}
printf("Blad sieci\n");
fclose(flog);
return 0;
}Zadanie do przemyślenia (na ćwiczenia 4)
Napisz serwer (pojedynczy proces z jednym wątkiem) wykorzystujący funkcję select(), który:Zadanie będzie wykonane poprawnie, jeżeli wszystkie dane zostaną przekopiowane poprawnie. Serwer potwierdzi poprawne wykonanie zadania pakietem UDP zawierającym "ok\n" (nie trzeba go kopiować). Uwaga: na jednym komputerze można uruchomić tylko jeden (jednego studenta) serwer, czas pomiędzy poszczególnymi uruchomieniami powinien wynosić co najmniej 30 sekund (jeżeli połączenie TCP zostało poprawnie zamknięte, w przeciwnym razie bezpieczniej kilka minut).
- nasłuchuje na porcie TCP 12346,
- rejestruje się (jako działający serwer) poprzez wysłanie pakietu UDP zawierającego dwucyfrowy numer grupy z USOS (11 — 1CA, 12 — 1CB, 13 — 1CC), spację, numer indeksu i znak nowego wiersza (np. "11 321321\n") na port 12346 serwera 150.254.77.101,
- po połączeniu klienta poprzez TCP przesyła wszystkie otrzymane od niego dane w paczkach po 100 bajtów przez UDP na port 12346, serwer 150.254.77.101 (jeżeli nie ma pełnych 100 bajtów, to czeka aż się uzbierają),
- jednocześnie przesyła wszystkie otrzymane przez UDP dane przez nawiązane wcześniej połączenie TCP,
- po 30 sekundach program kończy działanie.
Begin main navigation